package com.compomics.proteocloud.starter;

import java.beans.PropertyChangeSupport;
import java.io.File;
import java.io.IOException;
import java.net.ConnectException;
import java.security.NoSuchAlgorithmException;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.*;

import com.compomics.proteocloud.db.DBManager;
import com.compomics.proteocloud.db.accessor.*;
import com.compomics.proteocloud.ec2.EC2Instance;
import com.compomics.proteocloud.io.MascotGenericFile;
import com.compomics.proteocloud.jobs.CloudJob;
import com.compomics.proteocloud.model.*;
import com.compomics.proteocloud.ui.ProgressDialog;
import com.compomics.util.protein.Header;
import org.jets3t.service.S3ServiceException;
import org.jets3t.service.model.S3Bucket;
import org.jets3t.service.model.S3Object;
import org.jets3t.service.security.AWSCredentials;

import com.compomics.proteocloud.ec2.EC2Manager;
import com.compomics.proteocloud.io.FileProcessor;
import com.compomics.proteocloud.s3.S3Manager;
import com.compomics.proteocloud.s3.StorageObject;
import com.compomics.proteocloud.sqs.MessageManager;
import com.compomics.proteocloud.sqs.MessageObject;
import com.compomics.proteocloud.util.AWSUtils;
import com.xerox.amazonws.ec2.EC2Exception;
import com.xerox.amazonws.ec2.InstanceType;
import com.xerox.amazonws.ec2.ReservationDescription.Instance;
import com.xerox.amazonws.sqs2.Message;
import com.xerox.amazonws.sqs2.SQSException;

import javax.swing.*;

/**
 * This class contains all the useful features for the cloud work:
 * - Upload and Download of files (S3 storage)
 * - Send and Receive messages (SQS queue system)
 * - Launch and Terminate instances (EC2 elastic cloud system)
 * @author Thilo Muth
 *
 */
public class CloudTools {	
	
	private static Properties props;
	private static AWSCredentials awsCredentials;
	private static CloudTools cloudtools = null;
	public static final double POLLRATE = 30.0;
	public static final int JOBTIME = 3600;
    private boolean credentialsProvided = false;

    /**
     * The actual de novo search result.
     */
    private DenovoSearchResult denovoSearchResult;
    /**
     * The actual database search result.
     */
    private DbSearchResult dbSearchResult;

    /**
     * The database connection.
     */
    private Connection conn;
    /**
     *  Property change support for handling events and notifying the user interface about changes.
     */
    private PropertyChangeSupport pSupport;

    /**
	 * Default constructor
	 */
	private CloudTools() {
		init();
	}
	
	/**
	 * Loads the AWSCredentials and initializes the properties. 
	 * @throws S3ServiceException, IOException 
	 */
	private void init() {
        try {
            // Load the AWS Credentials
            awsCredentials = AWSUtils.loadAWSCredentials();

            // Get the Properties object.
            props = AWSUtils.getProperties();

            pSupport = new PropertyChangeSupport(this);
        } catch (IOException ioe) {
            ioe.printStackTrace();
            credentialsProvided = false;
        } catch (RuntimeException re){
            re.printStackTrace();
            credentialsProvided = false;
        }

        if(awsCredentials != null) {
            credentialsProvided = true;
        }
	}

    /**
     * Forwards a bound property update to any registered listeners.
     * No event is fired if old and new are equal and non-null.
     * @param propertyName The programmatic name of the property that was changed.
     * @param oldValue The old value of the property.
     * @param newValue The new value of the property.
     */
    public void firePropertyChange(String propertyName, Object oldValue, Object newValue) {
        pSupport.firePropertyChange(propertyName, oldValue, newValue);
    }

    /**
	 * Chunks the files into the specified size.
	 * @param files The name of the spectra file.
	 * @param offset Offset = chunk size.
	 * @return int The number of chunks created
	 */
	public List<File> chunkFiles(File[] files, int offset){
        firePropertyChange("new message", null, "Chunking Files");

		FileProcessor processor = new FileProcessor(files, new File(files[0].getParent()), offset);
		try {
            firePropertyChange("indeterminate", false, true);
			processor.write();
		} catch (IOException e) {
			e.printStackTrace();
		}
        firePropertyChange("close", null, "Chunking Files Finished");
		return processor.getChunkedFiles();
	}

    /**
     * Lists all the available S3 buckets.
     * @return S3 Bucket array.
     * @throws S3ServiceException
     */
    public S3Bucket[] listBuckets() {
        S3Manager s3manager = new S3Manager(awsCredentials);
        S3Bucket[] buckets = null;
        try {
            // List all your buckets.
            buckets =  s3manager.listBuckets();
        } catch (S3ServiceException s3e) {
            JOptionPane.showMessageDialog(null, "Wrong/missing value in the AWS properties:" + "\n" + "Please provide correct values in the proteocloud_empty.properties file.",
                    "", JOptionPane.ERROR_MESSAGE);
        }
        return buckets;
    }

	/**
	 * List the storage objects.
	 * @return StorageObject[]
	 */
	public StorageObject[] listStorageObjects(){
		S3Manager s3manager = new S3Manager(awsCredentials);
		S3Object[] s3Objects;
		StorageObject[] objects = null;
		try {
			s3Objects = s3manager.listObjects(s3manager.getBucket(props.getProperty(AWSUtils.BUCKET)).getName());
			objects = new StorageObject[s3Objects.length];
			for (int i = 0; i < s3Objects.length; i++){
				objects[i] = new StorageObject(s3Objects[i].getKey(), s3Objects[i].getContentLength(), s3Objects[i].getLastModifiedDate());
			}
		} catch (S3ServiceException s3e) {            
            JOptionPane.showMessageDialog(null, "Wrong/missing value in the AWS properties:" + "\n" + "Please provide correct values in the proteocloud_empty.properties file.",
                    "", JOptionPane.ERROR_MESSAGE);
			s3e.printStackTrace();
		}		
		return objects;
	}
	
	/**
	 * Lists the message objects from the queue.
	 * @return The message objects to be returned.
	 */
	public MessageObject[] getMessageObjects() {
        MessageObject[] objects = null;
        try {
		    MessageManager msgManager = new MessageManager(props, "dataQueue");
		    List<Message> messages = msgManager.receiveMessages();
				// Null check
				if(messages != null){
					objects = new MessageObject[messages.size()];
					for (int i = 0; i < messages.size(); i++){						
						objects[i] = new MessageObject(messages.get(i).getMessageId(), messages.get(i).getMessageBody(), messages.get(i).getReceiptHandle());
					}
				}
			} catch (SQSException e) {
				e.printStackTrace();
			}
		return objects;
	}
	
	/**
	 * List the job objects.
	 * @return CloudJob[]
	 */
	public CloudJob[] getJobObjects() {
        CloudJob[] clouds = null;
        try {
		    MessageManager msgManager = new MessageManager(props, "statusQueue");
		    List<Message> messages;

				messages = msgManager.receiveMessages();
				// Null check
				if(messages != null){
					clouds = new CloudJob[messages.size()];
					for (int i = 0; i < messages.size(); i++){
						clouds[i] = new CloudJob();
						StringTokenizer tokenizer = new StringTokenizer(messages.get(i).getMessageBody());
						List<String> tokenList = new ArrayList<String>();						
						
						// Iterate over all the tokens
						while (tokenizer.hasMoreTokens()) {
							tokenList.add(tokenizer.nextToken());
						}
						clouds[i].setInstanceID(tokenList.get(0));
						clouds[i].setCurrentJob(tokenList.get(1));
						msgManager.deleteMessage(messages.get(i));
					}
				}
			} catch (SQSException e) {
				e.printStackTrace();
			}
		return clouds;
	}

    /**
     * Returns the current database search result.
     * @param projectTitle The project title.
     * @return The current database search result.
     */
    public DbSearchResult getDbSearchResult(String projectTitle) throws ConnectException {
        if (dbSearchResult == null) {
            retrieveDbSearchResult(projectTitle);
        }
        return dbSearchResult;
    }

    /**
     * Returns the result(s) from the database search for a particular experiment.
     * @param projectTitle The project title.
     * @return DbSearchResult
     */
    private void retrieveDbSearchResult(String projectTitle) throws ConnectException {
        // Init the database connection.
        try {
            initDBConnection();

            // Set up the database search result.
            dbSearchResult = new DbSearchResult(projectTitle);


            firePropertyChange("indeterminate", false, true);
            
            List<SearchHit> searchHits = new ArrayList<SearchHit>();
            firePropertyChange("new message", null, "Retrieving X!Tandem Search Results");
            searchHits.addAll(XTandemhit.getHitsFromProjectTitle(projectTitle, conn));
            firePropertyChange("new message", null, "Retrieving Ommsa Search Results");
            searchHits.addAll(Omssahit.getHitsFromProjectTitle(projectTitle, conn));
            firePropertyChange("new message", null, "Retrieving Crux Search Results");
            searchHits.addAll(Cruxhit.getHitsFromProjectTitle(projectTitle, conn));
            firePropertyChange("new message", null, "Retrieving Inspect Search Results");
            searchHits.addAll(Inspecthit.getHitsFromProjectTitle(projectTitle, conn));
            firePropertyChange("new message", null, "Building Search Results Object");
            int progress = 0;
            firePropertyChange("indeterminate", true, false);
            firePropertyChange("new", 0, searchHits.size());
            for (SearchHit searchHit : searchHits) {
                addProteinSearchHit(searchHit);
                firePropertyChange("progress", 0, ++progress);
            }
        } catch (SQLException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * Returns the result from the de novo search.
     * @param projectTitle The project title.
     * @return DenovoSearchResult
     * @throws SQLException
     */
    public DenovoSearchResult getDenovoSearchResult(String projectTitle) throws ConnectException {
        if (denovoSearchResult == null) {
            retrieveDeNovoSearchResult(projectTitle);
        }
        return denovoSearchResult;
    }

    /**
     * This method retrieves the de novo result from the database for a specific project and experiment.
     * @param projectTitle The project title.
     */
    private void retrieveDeNovoSearchResult(String projectTitle) throws ConnectException {
        try {
            // Initialize the connection.
            initDBConnection();

            // The protein hit set, containing all information about found proteins.
            denovoSearchResult = new DenovoSearchResult(projectTitle);

            firePropertyChange("new message", null, "Retrieving De Novo Results");
            firePropertyChange("indeterminate", true, false);

            List<Pepnovohit> pepnovohits = Pepnovohit.getHitsFromProjectTitle(projectTitle, conn);
            int maxProgress = pepnovohits.size();
            int curProgress = 0;
            firePropertyChange("new", 0, maxProgress);

            for(Pepnovohit hit : pepnovohits) {
                denovoSearchResult.addHitSet(hit.getSpectrumName(), (int) hit.getL_spectrumid(), hit);
                firePropertyChange("progress", 0, ++curProgress);
            }

        } catch (SQLException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace(); 
        }
    }

    public MascotGenericFile getSpectrumFromSpectrumID(long spectrumID) throws SQLException, IOException, ConnectException {
        initDBConnection();
        Spectrumfile spectrumfile = Spectrumfile.findFromSpectrumID(spectrumID, conn);
        return new MascotGenericFile(spectrumfile.getFilename(), new String(spectrumfile.getUnzippedFile()));
    }

    /**
     * This method converts a search hit into a protein hit and adds it to the current protein hit set.
     * @param hit The search hit implementation.
     * @throws SQLException when the retrieval did not succeed.
     */
    private void addProteinSearchHit(SearchHit hit) throws SQLException, IOException {

        // Create the PeptideSpectrumMatch
        PeptideSpectrumMatch psm = new PeptideSpectrumMatch(hit.getSpectrumId(), hit);

        // Get the peptide hit.
        PeptideHit peptideHit = new PeptideHit(hit.getSequence(), psm);

        // Parse the annotation FASTA header
        Header header = Header.parseFromFASTA(hit.getProteinAnnotation());

        // Add the protein to the result set.
        dbSearchResult.addProtein(new ProteinHit(header.getAccession(), header.getDescription(), hit.getProteinSequence(), peptideHit));
    }

    /**
     * Sets the database connection.
     * @throws SQLException
     */
    public void initDBConnection() throws SQLException, ConnectException {
        // Connection conn
        if (conn == null) {
            // connect to database
            DBManager dbManager = new DBManager(awsCredentials, props.getProperty(AWSUtils.DATABASE_NAME));

            this.conn = dbManager.getConnection();
        }
    }

    /**
     * Clears the database connection.
     * @throws SQLException
     */
    public void closeDBConnection() throws SQLException {
        if (conn != null) {
            conn.close();
            conn = null;
        }
    }

    /**
     * This method retrieves all the projects in the database.
     * @return Projects as string array.
     */
    public String[] getProjects() throws ConnectException {
        String[] projects = null;

        try {
            // Initialize the connection.
            initDBConnection();

            ArrayList<ProjectTableAccessor> projectEntries = ProjectTableAccessor.retrieveAllEntries(conn);
            projects = new String[projectEntries.size()];

            for(int i = 0; i < projectEntries.size(); i++) {
                projects[i] = projectEntries.get(i).getTitle();
            }

        } catch (SQLException e) {
            e.printStackTrace();
        }
        return projects;
    }

    /**
     * Returns the project by title.
     * @param projectTitle
     * @return Project by title.
     * @throws ConnectException
     */
    public Project getProjectByTitle(String projectTitle) throws ConnectException {
        Project project = null;
        try {
            // Initialize the connection.
            initDBConnection();

            project = Project.findFromTitle(projectTitle, conn);
        } catch (SQLException e) {
            e.printStackTrace();
        }
        return project;

    }

    /**
     * Chunks the files into the specified size.
     * @param files The name of the spectra file.
     * @param size The size of the chunked file.
     * @return int The number of chunks created
     */
    public int fetchResults(File[] files, File folder, int size){
        FileProcessor processor = new FileProcessor(files, folder, size);
        try {
            processor.write();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return processor.getChunkNumber();
    }
	
	/**
	 * Upload files to the S3 storage specified by filename and bucketname.
	 */
	public void uploadFile(List<File> files){
		S3Manager s3manager = new S3Manager(awsCredentials);
        firePropertyChange("new message", null, "Uploading Files");
        int progress = 0;
        int maximum = files.size();
        double factor = 100.0 / (maximum * 1.0);

        firePropertyChange("indeterminate", true, false);
        firePropertyChange("new", 0, maximum);
		try {
            for(File file : files) {
                s3manager.upload(s3manager.getBucket(props.getProperty(AWSUtils.BUCKET)), file.getAbsolutePath());
                progress++;
                int prog = (int) (progress * factor);
                firePropertyChange("progress", 0, prog);
            }
            firePropertyChange("close", null, "Uploading Files Finished");
		} catch (S3ServiceException e) {				
			e.printStackTrace();
		} catch (NoSuchAlgorithmException e) {				
			e.printStackTrace();
		} catch (IOException e) {			
			e.printStackTrace();
		}
	}	
	
	/**
	 * Returns all instances as EC2Instance array.
	 * @return
	 */
	public EC2Instance[] getInstanceObjects(){
		EC2Manager ec2manager = new EC2Manager(awsCredentials);
		EC2Instance[] EC2s = null;
		List<Instance> instances;
			try {
				instances = ec2manager.getAllInstances();
				// Null check
				if(instances != null){
					EC2s = new EC2Instance[instances.size()];
					for (int i = 0; i < instances.size(); i++){
						EC2s[i] = new EC2Instance(instances.get(i).getInstanceId(), instances.get(i).getState(), instances.get(i).getImageId(), instances.get(i).getDnsName());
					}
				}
			} catch (EC2Exception e) {
				e.printStackTrace();
			}
		return EC2s;
	}
	
	/**
	 * Launches EC2 instance(s).
	 * @param numberOfInstances
	 */
	public void launchInstances(int numberOfInstances){
		EC2Manager ec2manager = new EC2Manager(awsCredentials);
		try {
			ec2manager.launchInstances(props.getProperty(AWSUtils.DEFAULT_WORKER_IMAGE), numberOfInstances, numberOfInstances, props.getProperty(AWSUtils.DEFAULT_KEYPAIR), props.getProperty(AWSUtils.DEFAULT_SECURITY_GROUP), props.getProperty(AWSUtils.ZONE), InstanceType.LARGE);

		} catch (EC2Exception e1) {
			e1.printStackTrace();
		}
	}
	
	/**
	 * Terminates a selected EC2 instances.
	 * @param instanceIds
	 */
	public void terminateInstances(String[] instanceIds){
		EC2Manager ec2manager = new EC2Manager(awsCredentials);
		try {
			ec2manager.terminateInstances(instanceIds);
		} catch (EC2Exception e1) {
			e1.printStackTrace();
		}
	}
	
	/**
	 * Returns the CloudTools Singleton object.
	 * @return cloudtools
	 */
	public static CloudTools getInstance() {
		if (cloudtools == null) {
			cloudtools = new CloudTools();
		}
		return cloudtools;
	}
	
	public static void main(String[] args) {
		CloudTools cloudtools = CloudTools.getInstance();
        MessageManager msgManager = null;
        try {
            msgManager = new MessageManager(props, "dataQueue");
        } catch (SQSException e) {
            JOptionPane.showMessageDialog(null, e.getLocalizedMessage() + "\n" + "Please make sure that you provided the credentials in the proteocloud_empty.properties.",
                    "", JOptionPane.ERROR_MESSAGE);
        }
        S3Manager s3manager = new S3Manager(awsCredentials, props.getProperty(AWSUtils.BUCKET));
	}

    /**
     * Are credentials provided.
     * @return True if credentials are provided
     */
    public boolean areCredentialsProvided() {
        return credentialsProvided;
    }

    /**
     * Adds a property change listener.
     * @param dialog The progress dialog
     */
    public void addPropertyChangeListener(ProgressDialog dialog) {
        pSupport.addPropertyChangeListener(dialog);
    }

    /**
     * This method creates a new project.
     * @param title The project title.
     * @param taxon The taxon / species.
     * @return Generated project ID.
     * @throws SQLException
     */
    public Long createNewProject(String title, String taxon, String fragIonTol, String precIonTol, String precUnit) throws SQLException {
        HashMap<Object, Object> data = new HashMap<Object, Object>(9);
        data.put(Project.TITLE, title);
        long taxonid = Taxon.findFromSpecies(taxon, conn).getTaxonid();
        data.put(Project.L_TAXONID, taxonid);
        data.put(Project.TAXON, taxon);
        data.put(Project.FRAGMENT_TOL, Double.valueOf(fragIonTol));
        data.put(Project.PRECURSOR_TOL, Double.valueOf(precIonTol));
        data.put(Project.PRECURSOR_UNIT, precUnit);
        Project project = new Project(data);
        project.persist(conn);
        return (Long) project.getGeneratedKeys()[0];
    }

    /**
     * Retrieves all the taxa.
     * @return Retrieved list of all taxa in the database.
     * @throws SQLException
     */
    public List<Taxon> retrieveAllTaxa() throws SQLException {
        if(conn != null) {
            return  Taxon.getAllTaxa(conn);
        } else
            return null;
    }

    /**
     * Returns the properties.
     * @return Properties object.
     */
    public static Properties getProperties() {
        return props;
    }
}
